(function () {
    "use strict";

    require("./assign");
    var EventBus = require("./event-bus");
    var PathWalker = require("./path-walker");
    var defineType = require("./define-type");

    function isNotifier(instance) {
        return instance instanceof Notifier;
    }

    function addToFrozenEvents(notifier, e) {
        var event = notifier.frozenEvents[e.property];
        if (event) {
            event.newValue = e.newValue;
            event.newItems = e.newItems;
            return;
        }

        notifier.frozenEvents[e.property] = e;
    }

    function createChangeListener(notifier, name) {
        return function (e) {
            var property = PathWalker.join(name, e.property);
            if (notifier.get(property) === notifier) {
                throw new Error("circular notification, " + e.property);
            }

            var e = Object.assign({}, e, {
                target: notifier,
                property: property
            });

            if (this.frozen) {
                addToFrozenEvents(this, e);
                return;
            }

            console.error("triggering from listener");
            notifier.trigger("change", e);
        }.bind(notifier);
    }

    function subscribeNotifier(value, listener) {
        if (isNotifier(value)) {
            value.on("change", listener);
        }
    }

    function unsubscribeNotifier(value, listener) {
        if (isNotifier(value)) {
            value.off("change", listener);
        }
    }

    function createProperty(notifier, name) {
        var value = notifier[name];

        var listener = null;
        if (isNotifier(value)) {
            listener = createChangeListener(notifier, name);
            subscribeNotifier(value, listener);
        }

        function get() {
            return value;
        }

        function set(newValue) {
            if (newValue === value) {
                return value;
            }

            var oldValue = value;
            unsubscribeNotifier(oldValue, listener);
            if (isNotifier(newValue)) {
                if (!listener) {
                    listener = createChangeListener(notifier, name);
                }

                subscribeNotifier(newValue, listener);
            }

            value = newValue;

            var e = {
                target: notifier,
                property: PathWalker.escapeName(name),
                oldValue: oldValue,
                newValue: newValue
            };

            if (this.frozen) {
                addToFrozenEvents(this, e);
            } else {
                console.error("triggering from prop change");
                notifier.trigger("change", e);
            }

            return value;
        }

        Object.defineProperty(notifier, name, {
            configurable: true,
            enumerable: true,
            get: get,
            set: set.bind(notifier)
        });
    }

    function createProperties(notifier) {
        for (var name in notifier) {
            switch (name) {
            case "frozen":
            case "frozenEvents":
            case "freeze":
                break;
            default:
                createProperty(notifier, name);
                break;
            }
        }
    }

    function onChange(e) {
        var handlerName = e.property + "_change";

        if (this[handlerName] instanceof Function) {
            this[handlerName](e);
        }

        if (this["onchange"] instanceof Function) {
            this["onchange"](e);
        }
    }

    function init(notifier) {
        notifier.on("change", onChange.bind(notifier));
    }

    var Notifier = defineType(function () {
        this.frozen = false;
        this.frozenEvents = {};
        createProperties(this);
        init(this);
    }, new EventBus, new PathWalker, {
        freeze: function (value) {
            this.frozen = value;
            if (!this.frozen) {
                var frozenEvents = this.frozenEvents;
                this.frozenEvents = {};
                console.error("triggering from unfreeze");
                for (var property in frozenEvents) {
                    this.trigger("change", frozenEvents[property]);
                }
            }
        }
    });

    Notifier.isNotifier = isNotifier;
    Notifier.addToFrozenEvents = addToFrozenEvents;

    module.exports = Notifier;
})();
